﻿using gt.rediscache.core;
using gt.rediscache.core.Clients;
using gt.rediscache.core.Connections.ClientServer;
using gt.rediscache.core.Entry;
using gt.rediscache.logger;
using gt.rediscache.plugins;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace gt.rediscache
{
    public sealed class RedisCacheManager
    {
        private static Dictionary<string, IRedisClient> m_clientContainer = new Dictionary<string, IRedisClient>();
        private static object m_obj = new object();
        private static RedisClientServerPoolFactory _clientServerPoolFactory = new RedisClientServerPoolFactory();
        private static RedisClientStateReporter<RedisClientInfo> _reporter = new RedisClientStateReporter<RedisClientInfo>();
        private static RedisClientNotifyer _notifyer = new RedisClientNotifyer();

        private RedisCacheManager()
        {
        }
        static RedisCacheManager()
        {
#if NET45
            var defaultLlogger = log4net.LogManager.GetLogger("RedisLogger");
#else
            var defaultLlogger = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType.Assembly, "RedisLogger");
#endif
            RedisLogManager.ReplaceLogger(new Log4netAdapter(defaultLlogger));
        }

        public static void InitCacheFromConfig(log4net.ILog logger = null)
        {
            InitCacheFromConfigAsync(logger).Wait();
        }

        public static Task InitCacheFromConfigAsync(log4net.ILog logger = null)
        {
            try
            {
                if (logger != null) RedisLogManager.ReplaceLogger(new Log4netAdapter(logger));

                RedisCacheConfig cacheConfig = RedisConfigManager.GetRedisCacheConfigFromCache();
                if (cacheConfig == null || cacheConfig.ApplicationCaches == null
                    || cacheConfig.ApplicationCaches.Count == 0)
                    throw new ArgumentNullException("config file init failed,please check config!");
                _reporter.SetReportApi(cacheConfig.RedisCacheUrl);
                _notifyer.SetNotifyUrl(cacheConfig.IDC, cacheConfig.NotifyUrl);
                List<Task> taskList = new List<Task>();
                foreach (var appCache in cacheConfig.ApplicationCaches)
                {
                    taskList.Add(Task.Run(() =>
                    {
                        GetClient(appCache.Name);
                    }));
                }

                return Task.WhenAll(taskList);
            }
            catch (Exception ex)
            {
                throw new Exception("init cache config failed:" + ex.Message);
            }
        }

        public static IRedisClient GetClient<T>()
            where T : ApplicationCacheConfig, new()
        {
            T tempEnity = new T();
            string appCacheName = tempEnity.GetAppCacheName();
            if (!m_clientContainer.ContainsKey(appCacheName))
            {
                lock (m_obj)
                {
                    if (!m_clientContainer.ContainsKey(appCacheName))
                    {
                        IRedisClient client = CreateClient(appCacheName);
                        m_clientContainer.Add(appCacheName, client);
                    }
                }
            }
            return m_clientContainer[appCacheName];
        }
        public static IRedisClient GetClient(string appCacheName)
        {
            if (!m_clientContainer.ContainsKey(appCacheName))
            {
                lock (m_obj)
                {
                    if (!m_clientContainer.ContainsKey(appCacheName))
                    {
                        IRedisClient client = CreateClient(appCacheName);
                        m_clientContainer.Add(appCacheName, client);
                    }
                }
            }
            return m_clientContainer[appCacheName];
        }
        public static List<IRedisClient> GetAllClients()
        {
            return m_clientContainer.Values.ToList();
        }

        public static void Close()
        {
            foreach (var client in m_clientContainer.Values)
            {
                client.Dispose();
            }
            m_clientContainer.Clear();
        }

        public static void Close(string appCacheName)
        {
            if (m_clientContainer.ContainsKey(appCacheName))
            {
                var client = m_clientContainer[appCacheName];
                if (client != null)
                {
                    client.Dispose();
                    m_clientContainer.Remove(appCacheName);
                }
            }
        }

        public static IRedisClient CreateClient(string applicationCacheName)
        {
            RedisCacheConfig cacheConfig = RedisConfigManager.GetRedisCacheConfigFromCache();
            if (cacheConfig == null || cacheConfig.ApplicationCaches == null
                || cacheConfig.ApplicationCaches.Count == 0)
                throw new ArgumentNullException("config file init failed,please check config!");

            ApplicationCacheConfig appCache = cacheConfig.ApplicationCaches.FirstOrDefault<ApplicationCacheConfig>(e =>
                string.Equals(e.Name, applicationCacheName, StringComparison.CurrentCultureIgnoreCase));

            if (appCache == null) throw new InvalidOperationException("can not find ApplicationCache by name:" + applicationCacheName);

            CheckApplicationCache(appCache);
            var options = ConvertToConfiguration(cacheConfig.ApplicationName, cacheConfig.IDC, appCache);
            var client = new SmartRedisClient(options, _clientServerPoolFactory);
            client.OnClientStateChangedEvent += (sender, args) =>
            {
                var eventClient = sender as IRedisClient;
                if (args.Type == 1 || args.Type == 2)
                {
                    var msg = $"redisclient:{eventClient.AppCacheName} redisClientServer:{args.ClientServer.Name} connect failed.";
                    _notifyer.NotifyAsync(msg, args.ClientServer.Name, args.Type == 2);
                }
                _reporter.ReportAsync(eventClient.RedisCacheInfo());
            };
            _reporter.ReportAsync(client.RedisCacheInfo());
            RedisLogManager.Info(string.Format("rediscache client {0} init success.", appCache.Name));

            return client;
        }

        private static void CheckApplicationCache(ApplicationCacheConfig appCache)
        {
            string mainCachePoolName = appCache.MainCache;
            if (string.IsNullOrEmpty(mainCachePoolName)) throw new InvalidOperationException("MainCache name can not be null!");

            CachePoolConfig cachePool = RedisConfigManager.GetCachePoolFromCache<CachePoolConfig>(mainCachePoolName);
            if (cachePool == null) throw new InvalidOperationException("can not find CachePool path by name:" + mainCachePoolName);

            CachePoolConfig backupCachePool;
            string backupCachePoolName = appCache.BackupCache;
            if (!string.IsNullOrEmpty(backupCachePoolName))
            {
                backupCachePool = RedisConfigManager.GetCachePoolFromCache<CachePoolConfig>(backupCachePoolName);
                if (backupCachePool == null) throw new InvalidOperationException("can not find backupCachePool path by name:" + backupCachePoolName);
            }
        }
        private static SmartRedisClientConfiguration ConvertToConfiguration(string applicationName, string idc, ApplicationCacheConfig appCache)
        {
            SmartRedisClientConfiguration configuration = new SmartRedisClientConfiguration();
            configuration.ApplicationName = applicationName;
            configuration.AllowSwitch = appCache.BackupMode == BackupModeEnum.Switch || appCache.BackupMode == BackupModeEnum.SwitchAndSync;
            configuration.AllowSync = appCache.BackupMode == BackupModeEnum.Sync || appCache.BackupMode == BackupModeEnum.SwitchAndSync;
            configuration.ClientName = appCache.Name;
            configuration.IDC = idc;
            configuration.Pool = ConvertToCacheConfiguration(applicationName, appCache.MainCache, appCache.DelayedRecoverySeconds, appCache.RedisFailoverWaitSeconds, appCache.MainCachePool);
            configuration.SwitchNodes = appCache.SwitchNodes;
            if (!string.IsNullOrEmpty(appCache.BackupCache) && appCache.BackupCachePool != null)
            {
                RedisClientConfiguration backupConfiguration = new RedisClientConfiguration();
                backupConfiguration.ApplicationName = applicationName;
                backupConfiguration.ClientName = appCache.Name;
                backupConfiguration.IDC = idc;
                backupConfiguration.Pool = ConvertToCacheConfiguration(applicationName, appCache.BackupCache, appCache.DelayedRecoverySeconds, appCache.RedisFailoverWaitSeconds, appCache.BackupCachePool);

                configuration.BackupOptions = backupConfiguration;
            }
            return configuration;
        }
        private static CachePoolConfiguration ConvertToCacheConfiguration(string applicationName, string poolName,
            int delayedRecoverySeconds, int failoverWaitSeconds, CachePoolConfig config)
        {
            return new CachePoolConfiguration
            {
                Name = poolName,
                Mode = config.Mode,
                DelayedRecoverySeconds = delayedRecoverySeconds,
                RedisFailoverWaitSeconds = failoverWaitSeconds,
                Nodes = config.Nodes.Select(x =>
                {
                    return new RedisNode
                    {
                        Name = x.Name,
                        Master = x.Master,
                        Slaves = x.Slaves,
                        DB = x.DB,
                        SlotFrom = x.SlotFrom,
                        SlotTo = x.SlotTo,
                        RedisConnectOptions = ParseToConfiguration(applicationName, x, config.Mode)
                    };
                }).ToList()
            };
        }
        private static ConfigurationOptions ParseToConfiguration(string applicationName, RedisNodeConfig node, PoolMode mode)
        {
            ConfigurationOptions configuration = new ConfigurationOptions();
            configuration.AbortOnConnectFail = false;
            configuration.KeepAlive = 10;
            configuration.ConnectTimeout = 1000;
            configuration.ReconnectRetryPolicy = new ExponentialRetry(1000);
            configuration.ConnectRetry = 3;
            configuration.EndPoints.Add(node.Master);
            configuration.AllowAdmin = true;
            if (!string.IsNullOrEmpty(applicationName))
                configuration.ClientName = applicationName;
            if (node.Slaves != null && node.Slaves.Count != 0)
            {
                foreach (var s in node.Slaves)
                {
                    if (!string.IsNullOrEmpty(s))
                        configuration.EndPoints.Add(s);
                }
            }
            if (!string.IsNullOrEmpty(node.Auth))
            {
                configuration.Password = node.Auth;
            }
            if (mode == PoolMode.Sentinel)
            {
                configuration.CommandMap = CommandMap.Sentinel;
                configuration.TieBreaker = string.Empty;
            }
            return configuration;
        }
    }
}
